#include <sobjc-commons/defines.h>
#include <sobjc-commons/utils.h>
#include <sobjc-commons/st.h>
#include "sobjc/runtime.h"
#include "sobjc/proto.h"
#include "sobjc/protocol.h"
#include "sobjc/selector.h"
#include "ivar.h"
#include "method.h"
#include "selector.h"

struct class_depend_list {
  ProtoClass proto;
  struct class_depend_list *next;
};

static BOOL inited = NO;
static ClassList class_list;
static st_table_t class_table;
static st_table_t proto_class_table;
static st_table_t class_depend_table;
static CategoryList cat_list;
static objc_lookup_class_f lookup_class_callback;

static Class try_load_class(ProtoClass);
static Class load_class(ProtoClass, Class);
static void reserve_loading_class(ProtoClass);
static Class make_class(ProtoClass, Class);
static void add_proto_methods_to_class(int, struct objc_proto_method *, Class);
static int lookup_imp_error(id recv, SEL sel, ...);

#ifdef OBJC_PROFILE
struct objc_cache_profile {
  unsigned long hit_count;
  unsigned long miss_count;
  unsigned long dup_count;
  unsigned long flush_count;
};

static struct objc_cache_profile cache_profile;
static void print_profile();
#endif /* OBJC_PROFILE */

#define CACHE_SIZE              4096
#define CACHE_MASK              (CACHE_SIZE-1)
#define CACHE_HASH(cls,sel)     ((((size_t)(cls)>>3) ^ (size_t)(sel)) & CACHE_MASK)

static Method meth_cache[CACHE_SIZE];
static Method super_meth_cache[CACHE_SIZE];

#define DEFAULT_CONST_STR_CLASS_NAME    "ConstantString"

const char *_objc_const_str_class_name = DEFAULT_CONST_STR_CLASS_NAME;
static BOOL const_str_class_name_is_default = YES;

void
_objc_set_const_str_class_name(const char *name)
{
  if (const_str_class_name_is_default) {
    const_str_class_name_is_default = NO;
    _objc_const_str_class_name = name;
  } else if (!STREQ(_objc_const_str_class_name, name))
    objc_log_error("cannot replace constant string class -- '%s'", name);
}

void objc_init_runtime()
{
  if (inited)
    return;
  else {
    objc_init_sel();
    class_table = st_init_strtable();
    proto_class_table = st_init_strtable();
    class_depend_table = st_init_strtable();
    MEMZERO(meth_cache, Method, CACHE_SIZE);
    MEMZERO(super_meth_cache, Method, CACHE_SIZE);

#ifdef OBJC_PROFILE
    MEMZERO(&cache_profile, struct objc_cache_profile, 1);
    atexit(print_profile);
#endif /* OBJC_PROFILE */

    inited = YES;
  }
}

void
objc_register_proto_class(ProtoClass proto)
{
  if (!try_load_class(proto))
    st_add_direct(proto_class_table, (st_data_t)proto->name, (st_data_t)proto);
}

static Class
try_load_class(ProtoClass proto)
{
  ProtoClass superproto;
  Class cls = NULL, superclass;
  struct class_depend_list *depends, *pre;

  if (!proto->super_name)
    cls = load_class(proto, NULL);
  else if (st_lookup(class_table,
                     (st_data_t)proto->super_name, 
                     (st_data_t *)&superclass))
    cls = load_class(proto, superclass);
  else if (st_lookup(proto_class_table,
                     (st_data_t)proto->super_name,
                     (st_data_t *)&superproto)) {
    superclass = try_load_class(superproto);
    if (superclass)
      cls = load_class(proto, superclass);
  }

  if (!cls && !st_lookup(class_depend_table, (st_data_t)proto->super_name, NULL))
    reserve_loading_class(proto);

  /* loading dependents */
  if (st_lookup(class_depend_table, (st_data_t)proto->name, &depends)) {
    while (depends) {
      try_load_class(depends->proto);
      pre = depends;
      depends = depends->next;
      objc_free(pre);
    }
    st_delete(class_depend_table, (st_data_t)proto->name, NULL);
  }

  return cls;
}

static Class
load_class(ProtoClass proto, Class superclass)
{
  Class cls;

  cls = make_class(proto, superclass);
  objc_add_class(cls);
  return cls;
}

static void
reserve_loading_class(ProtoClass proto)
{
  struct class_depend_list *depends, *next = NULL;

  st_lookup(class_depend_table, (st_data_t)proto->super_name,
            (st_data_t *)&next);
  depends = ALLOC(struct class_depend_list);
  depends->proto = proto;
  depends->next = next;
  st_add_direct(class_depend_table, (st_data_t)proto->super_name,
                (st_data_t)depends);
}

ClassList
objc_get_class_list()
{
  return class_list;
}

Class
objc_lookup_class(const char *name)
{
  static SEL sel;
  Class cls = NULL;
  Method meth;

  if (!st_lookup(class_table, (st_data_t)name, (st_data_t *)&cls) &&
      lookup_class_callback != NULL)
    cls = lookup_class_callback(name);

  if (cls && !(cls->flags & OBJC_CLS_INITIALIZED)) {
    if (!sel)
      sel = objc_register_sel("initialize");
    meth = objc_class_get_class_method(cls, sel);
    if (meth) {
      cls->flags |= OBJC_CLS_INITIALIZING;
      meth->imp((id)cls, sel);
      cls->flags |= OBJC_CLS_INITIALIZED;
      cls->flags ^= OBJC_CLS_INITIALIZING;
    }
  }

  return cls;
}

static
Class make_class(ProtoClass proto, Class superclass)
{
  Class cls;
  size_t inst_size = 0;
  IvarList ivars = NULL;
  Ivar ivar;
  ProtocolList protocols = NULL;
  int i;

  /* protocols */
  /* instance variables */
  for (i = 0; i < proto->nivars; i++) {
    ivar = objc_make_ivar(proto->ivars[i].name,
                          proto->ivars[i].type,
                          (ptrdiff_t)inst_size);
    ivars = objc_ivar_list_add(ivars, ivar);
    inst_size += proto->ivars[i].size;
  }

  cls = objc_make_class(proto->name, superclass, inst_size,
                        ivars, protocols, proto->ninst_dcodes);
  cls->isa = objc_make_metaclass(cls, proto->nclass_dcodes);

  /* methods */
  add_proto_methods_to_class(proto->ninst_meths,
                             proto->inst_meths, cls);
  add_proto_methods_to_class(proto->nclass_meths,
                             proto->class_meths, cls->isa);

  return cls;
}

static void
add_proto_methods_to_class(int nmeths,
                           struct objc_proto_method *proto_meths,
                           Class cls)
{
  Method meth;
  int i;

  for (i = 0; i < nmeths; i++) {
    meth = objc_make_method(proto_meths[i].name,
                            proto_meths[i].types,
                            proto_meths[i].imp);
    objc_class_add_method(cls, meth, proto_meths[i].dcode);
  }
}

void
objc_set_lookup_class_callback(objc_lookup_class_f callback)
{
  lookup_class_callback = callback;
}

void
objc_add_class(Class cls)
{
  static SEL sel;
  Method meth;

  class_list = objc_class_list_add(class_list, cls);
  st_add_direct(class_table, (st_data_t)cls->name, (st_data_t)cls);

  if (!sel)
    sel = objc_register_sel("load");
  meth = objc_class_get_class_method(cls, sel);
  if (meth)
    meth->imp((id)cls, sel);
}

void
objc_register_category(Category cat)
{
  /* FIXME */
  cat_list = objc_category_list_add(cat_list, cat);
}

IMP
objc_lookup_imp(id recv, SEL sel)
{
  Method meth;
  Class cls;

  if (recv == nil)
    return nil;

  cls = recv->isa;
  meth = meth_cache[CACHE_HASH(cls, sel)];
  if (meth != NULL && meth->meth_class == cls) {
#ifdef OBJC_PROFILE
    cache_profile.hit_count++;
    sel->send_count++;
#endif /* OBJC_PROFILE */
    return meth->imp;
  }

#ifdef OBJC_PROFILE
  if (meth != NULL)
    cache_profile.dup_count++;
#endif /* OBJC_PROFILE */

  do {
    meth = objc_class_get_instance_method(cls, sel);
    if (meth != NULL) {
      /* register cache for receiver class */
      meth_cache[CACHE_HASH(recv->isa, sel)] = meth;
#ifdef OBJC_PROFILE
      cache_profile.miss_count++;
#endif /* OBJC_PROFILE */
      return meth->imp;
    } else {
      cls = cls->superclass;
    }
  } while (cls != NULL);
  return (IMP)lookup_imp_error;
}

IMP
objc_lookup_imp_super(Class superclass, id recv, SEL sel)
{
  Method meth;
  Class cls;

  if (recv == nil)
    return nil;

  cls = superclass;
  do {
    meth = super_meth_cache[CACHE_HASH(cls, sel)];
    if (meth != NULL && meth->meth_class == cls)
      return meth->imp;
    else {
      meth = objc_class_get_instance_method(cls, sel);
      if (meth != NULL) {
        /* register cache for receiver superclass */
        super_meth_cache[CACHE_HASH(superclass, sel)] = meth;
        return meth->imp;
      }
      cls = cls->superclass;
    }
  } while (cls != NULL);
  return (IMP)lookup_imp_error;
}

static int
lookup_imp_error(id recv, SEL sel, ...)
{
  printf("cannot lookup message %s for %s\n", sel->name, recv->isa->name);
  return 0;
}

id
objc_nil_return(id recv __attribute__((unused)),
                SEL sel __attribute__((unused)), ...)
{
  return nil;
}

id
objc_fail_dispatch(id recv, SEL sel, ...)
{
  printf("cannot dispatch '%s' for '%s'\n", sel->name, recv->isa->name);
  return nil;
}

#ifdef OBJC_PROFILE

static void
print_profile()
{
  unsigned long total;

  /* cache profile */
  total = cache_profile.hit_count + cache_profile.miss_count;
  printf("Method cache:\n");
  printf("    Total lookup:     %lu\n", total);
  printf("    Lookup success:   %lu (%.2f%%)\n", cache_profile.hit_count,
         (double)cache_profile.hit_count / total * 100);
  printf("    Lookup miss:      %lu (%.2f%%)\n", cache_profile.miss_count,
         (double)cache_profile.miss_count / total * 100);
  printf("    Duplicate:        %lu (%.2f%%)\n", cache_profile.dup_count,
         (double)cache_profile.dup_count / total * 100);
}

#endif /* OBJC_PROFILE */

